package jinyilw.tools.net.dev;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public abstract class NetworkPacket implements Serializable, Cloneable
{
	private static final long serialVersionUID = -3147866363326288170L;
	public static final Class<String> STRING_TYPE = String.class;
	public static final Class<NetworkPacket> NETWORK_PACKET_TYPE = NetworkPacket.class;
	public static short NULL_ID = Short.MIN_VALUE;
	private static short UNIQUE_CODE = 0;

	protected static synchronized short getUniqueCode()
	{
		if (NetworkPacket.UNIQUE_CODE >= Short.MAX_VALUE)
		{
			NetworkPacket.UNIQUE_CODE = 0;
		}
		return ++NetworkPacket.UNIQUE_CODE;
	}

	private short id = NetworkPacket.NULL_ID;
	private short code;
	private short sender;

	private boolean consumed = NetworkConfig.isAutoConsumed();
	private boolean compressed = NetworkConfig.isDefaultCompressed();
	private int expiredTime = NetworkConfig.getDefaultExpiredTime();

	private boolean sendCode = NetworkConfig.isDefaultSendCode();
	private boolean sendSender = NetworkConfig.isDefaultSendSender();

	private long receivedTime;

	public NetworkPacket()
	{
	}

	protected final void readPacket(DataInputStream input) throws IOException
	{
		this.receivedTime = System.currentTimeMillis();

		if (this.sendCode)
		{
			this.code = input.readShort();
		}
		if (this.sendSender)
		{
			this.sender = input.readShort();
		}

		this.read(input);
	}

	protected final void writePacket(DataOutputStream output) throws IOException
	{
		if (this.sendCode)
		{
			output.writeShort(this.code);
		}
		if (this.sendSender)
		{
			output.writeShort(this.sender);
		}

		this.write(output);
	}

	public void read(DataInputStream input) throws IOException
	{
		Field[] fields = this.getClass().getDeclaredFields();
		for (Field field : fields)
		{
			int modifiers = field.getModifiers();

			if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)
					|| Modifier.isTransient(modifiers))
			{
				continue;
			}
			Class<?> fieldClass = field.getType();

			try
			{
				if (fieldClass.isPrimitive())
				{
					if (fieldClass == Integer.TYPE)
					{
						field.setInt(this, input.readInt());

					} else if (fieldClass == Double.TYPE)
					{
						field.setDouble(this, input.readDouble());

					} else if (fieldClass == Boolean.TYPE)
					{
						field.setBoolean(this, input.readBoolean());

					} else if (fieldClass == Float.TYPE)
					{
						field.setFloat(this, input.readFloat());

					} else if (fieldClass == Long.TYPE)
					{
						field.setLong(this, input.readLong());

					} else if (fieldClass == Short.TYPE)
					{
						field.setShort(this, input.readShort());

					} else if (fieldClass == Byte.TYPE)
					{
						field.setByte(this, input.readByte());

					} else if (fieldClass == Character.TYPE)
					{
						field.setChar(this, input.readChar());
					}
				} else if (fieldClass.isArray())
				{
					short length = input.readShort();
					fieldClass = fieldClass.getComponentType();

					Object arr = Array.newInstance(fieldClass, length);
					if (fieldClass.isPrimitive())
					{
						if (fieldClass == Integer.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setInt(arr, j, input.readInt());
							}

						} else if (fieldClass == Double.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setDouble(arr, j, input.readDouble());
							}

						} else if (fieldClass == Boolean.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setBoolean(arr, j, input.readBoolean());
							}

						} else if (fieldClass == Float.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setFloat(arr, j, input.readFloat());
							}

						} else if (fieldClass == Long.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setLong(arr, j, input.readLong());
							}

						} else if (fieldClass == Short.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setShort(arr, j, input.readShort());
							}

						} else if (fieldClass == Byte.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setByte(arr, j, input.readByte());
							}

						} else if (fieldClass == Character.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								Array.setChar(arr, j, input.readChar());
							}
						}
					} else if (NetworkPacket.NETWORK_PACKET_TYPE
							.isAssignableFrom(fieldClass))
					{
						for (int j = 0; j < length; j++)
						{
							NetworkPacket innerPacket = (NetworkPacket) fieldClass
									.newInstance();
							innerPacket.read(input);

							Array.set(arr, j, innerPacket);
						}
					} else
					{
						for (int j = 0; j < length; j++)
						{
							Array.set(arr, j, input.readUTF());
						}
					}
					field.set(this, arr);
				} else if (NetworkPacket.NETWORK_PACKET_TYPE
						.isAssignableFrom(fieldClass))
				{
					NetworkPacket innerPacket = (NetworkPacket) fieldClass
							.newInstance();
					innerPacket.read(input);
					field.set(this, innerPacket);
				} else
				{
					field.set(this, input.readUTF());
				}

			} catch (Exception ex)
			{
				throw new IOException(ex.getMessage());
			}
		}
	}

	public void write(DataOutputStream output) throws IOException
	{
		Field[] fields = this.getClass().getDeclaredFields();
		for (Field field : fields)
		{
			int modifiers = field.getModifiers();

			if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)
					|| Modifier.isTransient(modifiers))
			{
				continue;
			}

			Class<?> fieldClass = field.getType();

			try
			{
				if (fieldClass.isPrimitive())
				{
					if (fieldClass == Integer.TYPE)
					{
						output.writeInt(field.getInt(this));

					} else if (fieldClass == Double.TYPE)
					{
						output.writeDouble(field.getDouble(this));

					} else if (fieldClass == Boolean.TYPE)
					{
						output.writeBoolean(field.getBoolean(this));

					} else if (fieldClass == Float.TYPE)
					{
						output.writeFloat(field.getFloat(this));

					} else if (fieldClass == Long.TYPE)
					{
						output.writeLong(field.getLong(this));

					} else if (fieldClass == Short.TYPE)
					{
						output.writeShort(field.getShort(this));

					} else if (fieldClass == Byte.TYPE)
					{
						output.writeByte(field.getByte(this));

					} else if (fieldClass == Character.TYPE)
					{
						output.writeChar(field.getChar(this));
					}
				} else if (fieldClass.isArray())
				{
					Object arr = field.get(this);
					short length = (short) Array.getLength(arr);
					output.writeShort(length);

					fieldClass = fieldClass.getComponentType();

					if (fieldClass.isPrimitive())
					{
						if (fieldClass == Integer.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeInt(Array.getInt(arr, j));
							}
						} else if (fieldClass == Double.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeDouble(Array.getDouble(arr, j));
							}
						} else if (fieldClass == Boolean.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeBoolean(Array.getBoolean(arr, j));
							}
						} else if (fieldClass == Float.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeFloat(Array.getFloat(arr, j));
							}
						} else if (fieldClass == Long.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeLong(Array.getLong(arr, j));
							}
						} else if (fieldClass == Short.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeShort(Array.getShort(arr, j));
							}
						} else if (fieldClass == Byte.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeByte(Array.getByte(arr, j));
							}
						} else if (fieldClass == Character.TYPE)
						{
							for (int j = 0; j < length; j++)
							{
								output.writeChar(Array.getChar(arr, j));
							}
						}
					} else if (NetworkPacket.NETWORK_PACKET_TYPE
							.isAssignableFrom(fieldClass))
					{
						if (fieldClass
								.equals(NetworkPacket.NETWORK_PACKET_TYPE))
						{
							throw new UnsupportedOperationException(
									"Field array " + field.getName()
											+ " must be subclass of NetworkPacket, "
											+ "can not use the NetworkPacket class directly.");
						}

						for (int j = 0; j < length; j++)
						{
							NetworkPacket innerPacket = (NetworkPacket) Array
									.get(arr, j);
							innerPacket.write(output);
						}
					} else if (NetworkPacket.STRING_TYPE
							.isAssignableFrom(fieldClass))
					{
						for (int j = 0; j < length; j++)
						{
							output.writeUTF((String) Array.get(arr, j));
						}
					} else
					{
						throw new UnsupportedOperationException("Field array "
								+ field.getName() + " must be primitive, "
								+ "or String class, or subclass of NetworkPacket class.");
					}

					field.set(this, arr);
				} else if (NetworkPacket.NETWORK_PACKET_TYPE
						.isAssignableFrom(fieldClass))
				{
					if (fieldClass.equals(NetworkPacket.NETWORK_PACKET_TYPE))
					{
						throw new UnsupportedOperationException("Field "
								+ field.getName()
								+ " must be subclass of NetworkPacket, "
								+ "can not use the NetworkPacket class directly.");
					}

					NetworkPacket innerPacket = (NetworkPacket) field
							.get(this);
					innerPacket.write(output);
				} else if (NetworkPacket.STRING_TYPE
						.isAssignableFrom(fieldClass))
				{
					output.writeUTF((String) field.get(this));
				} else
				{
					throw new UnsupportedOperationException("Field "
							+ field.getName() + " must be primitive, "
							+ "or String class, or subclass of NetworkPacket class.");
				}

			} catch (Exception ex)
			{
				throw new IOException(ex.getMessage());
			}
		}
	}

	public void consume()
	{
		this.consumed = true;
	}

	public boolean isExpired()
	{
		if (this.expiredTime == -1)
		{
			return false;
		}

		return ((System.currentTimeMillis()
				- this.receivedTime) > this.expiredTime);
	}

	public short getID()
	{
		return this.id;
	}

	protected void setId(short id)
	{
		this.id = id;
	}

	public short generateCode()
	{
		this.setCode(NetworkPacket.getUniqueCode());

		return this.code;
	}

	public short getCode()
	{
		if (!this.sendCode)
		{
			throw new RuntimeException(
					"In order to get packet code, packet send code must be set to true.");
		}
		return this.code;
	}

	public void setCode(NetworkPacket retrievedPacketCode)
	{
		this.setCode(retrievedPacketCode.code);
	}

	protected void setCode(short code)
	{
		this.code = code;

		if (!this.sendCode)
		{
			if (!this.getClass().equals(NetworkObject.class))
			{
				throw new RuntimeException(
						"In order to set packet code, packet send code must be set to true.");
			} else
			{
				this.sendCode = true;
			}
		}
	}

	public short getSender()
	{
		if (!this.sendSender)
		{
			throw new RuntimeException(
					"In order to get packet sender, packet send sender must be set to true.");
		}
		return this.sender;
	}

	public NetworkPacket setSender(BaseClient sender)
	{
		this.setSender(sender.getClientID());

		return this;
	}

	protected void setSender(short sender)
	{
		this.sender = sender;

		this.sendSender = true;
	}
	public boolean isConsumed()
	{
		return this.consumed;
	}

	public NetworkPacket setConsumed(boolean consumed)
	{
		this.consumed = consumed;
		return this;
	}

	public boolean isCompressed()
	{
		return this.compressed;
	}

	public NetworkPacket setCompressed(boolean compressed)
	{
		this.compressed = compressed;
		return this;
	}

	public int getExpiredTime()
	{
		return this.expiredTime;
	}

	public NetworkPacket setExpiredTime(int expiredTime)
	{
		this.expiredTime = expiredTime;
		return this;
	}

	public boolean isSendCode()
	{
		return this.sendCode;
	}

	public NetworkPacket setSendCode(boolean sendCode)
	{
		this.sendCode = sendCode;
		return this;
	}

	public boolean isSendSender()
	{
		return this.sendSender;
	}

	public NetworkPacket setSendSender(boolean sendSender)
	{
		this.sendSender = sendSender;
		return this;
	}

	@Override
	public Object clone() throws CloneNotSupportedException
	{
		return super.clone();
	}

	protected static boolean useComma;

	@Override
	public String toString()
	{
		if (!NetworkConfig.DEBUG)
		{
			return super.toString();
		}

		StringBuilder buff = new StringBuilder();

		String packetClass = this.getClass().getName();
		if (packetClass.lastIndexOf('.') != -1)
		{
			packetClass = packetClass
					.substring(packetClass.lastIndexOf('.') + 1);
		}
		buff.append("Packet");
		if (this.sendSender)
		{
			buff.append(" Sender ID ").append(this.sender);
		}
		buff.append(": ").append(packetClass);
		if (this.sendCode)
		{
			buff.append(" (code=").append(this.code).append(")");
		}
		buff.append((NetworkPacket.useComma) ? " -> " : "\n");

		Field[] fields = this.getClass().getDeclaredFields();
		for (Field field : fields)
		{
			int modifiers = field.getModifiers();
			if (Modifier.isPrivate(modifiers) || Modifier.isFinal(modifiers)
					|| Modifier.isStatic(modifiers)
					|| Modifier.isTransient(modifiers))
			{
				continue;
			}

			try
			{
				Class<?> cls = field.getType();
				if (!cls.isArray())
				{
					String fieldClass = cls.getName();
					if (fieldClass.lastIndexOf('.') != -1)
					{
						fieldClass = fieldClass
								.substring(fieldClass.lastIndexOf('.') + 1);
					}
					if (!NetworkPacket.useComma)
					{
						buff.append("> ");
					}
					buff.append(fieldClass).append(" ")
							.append(field.getName()).append(" = ")
							.append(field.get(this));
					buff.append((NetworkPacket.useComma) ? ", " : "\n");
				} else
				{
					cls = cls.getComponentType();
					String fieldClass = cls.getName();
					if (fieldClass.lastIndexOf('.') != -1)
					{
						fieldClass = fieldClass
								.substring(fieldClass.lastIndexOf('.') + 1);
					}
					if (!NetworkPacket.useComma)
					{
						buff.append("> ");
					}
					buff.append(fieldClass).append("[] ")
							.append(field.getName()).append(" = ");

					Object arr = field.get(this);
					if (arr == null)
					{
						buff.append("null");
					} else
					{
						buff.append("[ ");
						int len = Array.getLength(arr);
						for (int j = 0; j < len; j++)
						{
							NetworkPacket.useComma = true;

							buff.append(Array.get(arr, j));
							if (j < len - 1)
							{
								buff.append("; ");
							}
						}
						NetworkPacket.useComma = false;
						buff.append(" ]");
					}
					buff.append((NetworkPacket.useComma) ? ", " : "\n");
				}
			} catch (Exception ex)
			{
				ex.printStackTrace();
			}
		}
		buff.append("End-Packet");
		NetworkPacket.useComma = false;
		return buff.toString();
	}
}
